package chartCraft;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Timer;
import java.util.TimerTask;

import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;

import chartCraft.Log;
import chartCraft.command.CommandBroker;
import chartCraft.command.CommandBroker.Command;
import chartCraft.graphDesk.CanvasPane;
import chartCraft.graphDesk.DraggableChart;
import chartCraft.model.LocationModel;
import chartCraft.model.Model;
import chartCraft.model.TopModel;
import chartCraft.tableDesk.AddTimeButton;
import chartCraft.tableDesk.ChartCraftTopMenuBar;
import chartCraft.tableDesk.ChartsTable;
import chartCraft.tableDesk.ObserverFrame;
import chartCraft.tableDesk.PlayButton;
import chartCraft.tableDesk.RemoveTimeButton;
import chartCraft.tableDesk.TableButtonsPane;
import chartCraft.tableDesk.TimeSlider;


public class ChartCraftController {
	public static final String APP_NAME = "ChartCraft";

	private static final int CANVAS_WIDTH = 600;
	private static final int CANVAS_HEIGHT = 400;
	private static final int TABLE_WIDTH = 600;
	private static final int TABLE_HEIGHT = 400;

	public static final boolean CANCEL = false;
	public static final boolean DONE = true;


	private final TopModel mTopModel;
	private final CommandBroker mCommandBroker;

	private Timer mPlayer;

	final JFileChooser fileChooser = new JFileChooser();
	private JFileChooser imageFileChooser = new JFileChooser();


	//For file saving
	private SaveStateManager mSaveState;


	/** Some important views */
	private JFrame mGraphFrame;
	private CanvasPane mCanvasPane;
	private JFrame mTableFrame;
	private ChartsTable mChartsTable;
	private TimeSlider mTimeSlider;


	public static ChartCraftController OpenFromTemplate(InputStream is) {
		ObjectInputStream in = null;
		TopModel topModel = null;
		try {
			in = new ObjectInputStream(is);
			topModel = (TopModel) in.readObject();
			in.close();
		} catch(Exception ex) {
			JOptionPane.showMessageDialog(null,
					"Failed to open the document. (Only accepts ChartCraft files.)",
					"Error",
					JOptionPane.ERROR_MESSAGE);
			return null;
		} 

		if (topModel == null) {
			JOptionPane.showMessageDialog(null,
					"Failed to open the document. (Only accepts ChartCraft files.)",
					"Error",
					JOptionPane.ERROR_MESSAGE);
			return null;
		}

		return new ChartCraftController(topModel, null);
	}

	public static ChartCraftController OpenFromFile(File file) {
		FileInputStream fis = null;
		ObjectInputStream in = null;
		TopModel topModel = null;
		try {
			fis = new FileInputStream(file);
			in = new ObjectInputStream(fis);
			topModel = (TopModel) in.readObject();
			in.close();
		} catch(Exception ex) {
			JOptionPane.showMessageDialog(null,
					"Failed to open the document. (Only accepts ChartCraft files.)",
					"Error",
					JOptionPane.ERROR_MESSAGE);
			return null;
		} 

		if (topModel == null) {
			JOptionPane.showMessageDialog(null,
					"Failed to open the document. (Only accepts ChartCraft files.)",
					"Error",
					JOptionPane.ERROR_MESSAGE);
			return null;
		}

		return new ChartCraftController(topModel, file);
	}



	/* Fresh start */
	public ChartCraftController() {
		mTopModel = new TopModel();
		mSaveState = new SaveStateManager(true);
		mCommandBroker = new CommandBroker(mSaveState);

		mTopModel.setNoOfTime(1);
		init();

		initValue();
		mSaveState.touch();
	}

	/* Load from saved model */
	public ChartCraftController(TopModel topModel, File file) {
		mSaveState = new SaveStateManager(false);
		mSaveState.resetCurrentDocument(file);
		mTopModel = topModel;
		mCommandBroker = new CommandBroker(mSaveState);

		init();
		mSaveState.touch();
	}

	private void init() {

		createViews();

		registerShortcutKey();
		showView();
	}

	private void initValue() {
		mTopModel.addColorForSeries(getNextColor());
		mTopModel.addNameForSeries(getNextSeriesName());

		LocationModel newModel = new LocationModel(getNextLocationPoint(), new Dimension(100, 100), mTopModel.getChartType());
		newModel.setName(getNextLocationName());

		newModel.insertNewSeries();

		mTopModel.insertNewLocationModel(newModel);
		mTopModel.addDepth(newModel, 0);
		updateBoundValue();
	}





	private void createViews() {
		//canvas views
		mGraphFrame = new ObserverFrame(){
			@Override
			public void update(Observable o, Object arg) {
				if (o instanceof SaveStateManager) {
					SaveStateManager saveState = (SaveStateManager) o;
					StringBuilder title = new StringBuilder();

					title.append("Chart Canvas - ");

					if (saveState.isDirty()) {
						title.append("*");
					}

					if (saveState.getCurrentDocument() == null) {
						title.append("Untitled Document");
					} else {
						title.append(saveState.getCurrentDocument().getName());
					}

					title.append(" - ");
					title.append(APP_NAME);
					setTitle(title.toString());
				}
			}
		};
		mSaveState.addObserver((Observer) mGraphFrame);
		mCanvasPane = new CanvasPane(this, mTopModel);
		JScrollPane scrollPane = new JScrollPane(mCanvasPane);

		mGraphFrame.getContentPane().add(scrollPane, BorderLayout.CENTER);

		mGraphFrame.setMinimumSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));
		mGraphFrame.setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));
		mGraphFrame.setMaximumSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));
		mGraphFrame.setResizable(false);

		mGraphFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		mGraphFrame.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				quit();
			}
		});

		//table views
		mTableFrame = new ObserverFrame(){
			@Override
			public void update(Observable o, Object arg) {
				if (o instanceof SaveStateManager) {
					SaveStateManager saveState = (SaveStateManager) o;
					StringBuilder title = new StringBuilder();

					title.append("Data Table - ");

					if (saveState.isDirty()) {
						title.append("*");
					}

					if (saveState.getCurrentDocument() == null) {
						title.append("Untitled Document");
					} else {
						title.append(saveState.getCurrentDocument().getName());
					}

					title.append(" - ");
					title.append(APP_NAME);
					setTitle(title.toString());
				}
			}
		};
		mSaveState.addObserver((Observer) mTableFrame);		
		mTableFrame.setMinimumSize(new Dimension(TABLE_WIDTH, TABLE_HEIGHT));

		mChartsTable = new ChartsTable(this, mTopModel);
		scrollPane = new JScrollPane(mChartsTable);
		mTableFrame.getContentPane().add(scrollPane, BorderLayout.CENTER);
		mTableFrame.setLocation(CANVAS_WIDTH, 0);

		//table buttons
		mTableFrame.getContentPane().add(new TableButtonsPane(this, mTopModel), BorderLayout.NORTH);


		//time slider
		JButton addATime = new AddTimeButton(this, mTopModel);
		JButton removeATime = new RemoveTimeButton(this, mTopModel);
		mTimeSlider = new TimeSlider(this, mTopModel);
		JButton playButton = new PlayButton(this, mTopModel);
		JPanel timePanel = new JPanel();
		timePanel.setLayout(new BoxLayout(timePanel, BoxLayout.X_AXIS));
		timePanel.add(removeATime);
		timePanel.add(addATime);
		timePanel.add(mTimeSlider);
		timePanel.add(playButton);
		mTableFrame.getContentPane().add(timePanel, BorderLayout.SOUTH);

		//top menu
		mTableFrame.setJMenuBar(new ChartCraftTopMenuBar(this, mTopModel, mSaveState, mCommandBroker));

		mTableFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		mTableFrame.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				quit();
			}
		});
	}

	private String getNextLocationName() {
		for(int i = 1; i <= mTopModel.getNoOfLocations() +1; i++) {
			String trial = "Chart Item " + i;
			if (!isTakenUpByLocationModel(trial)) {
				return trial;
			}
		}
		return "Chart Item";
	}

	private boolean isTakenUpByLocationModel(String name) {
		for (LocationModel locaitonModel: mTopModel.getAllLocationModels()) {
			if (locaitonModel.getName().equals(name)) {
				return true;
			}
		}
		return false;
	}

	private String getNextSeriesName() {
		for(int i = 1; i <= mTopModel.getNoOfSeries() +1; i++) {
			String trial = "Series " + i;
			if (!mTopModel.getSeriesNames().contains(trial)) {
				return trial;
			}
		}
		return "Series";
	}	



	private Color getNextColor() {
		Color colors[] = { new Color(0, 0, 255), new Color(255, 0, 25),
				new Color(0, 255, 0), new Color(102, 102, 0), new Color(205, 102, 0),
				new Color(102, 255, 255), new Color(255, 153, 153), new Color(153, 255, 153), 
				new Color(255, 255, 0), new Color(255, 0, 255)};

		for (int offset = 0; offset < colors.length; offset++) {
			Color trail = colors[offset];
			if(!mTopModel.getSeriesColors().contains(trail)) {
				return trail;
			}
		}

		return colors[0];
	}



	private Point getNextLocationPoint() {
		for (int posXY = 0; posXY < Math.min(CANVAS_HEIGHT, CANVAS_WIDTH); posXY+=5) {
			Point thisTrail = new Point(posXY, posXY);
			if(!isTakenUpByLocationModel(thisTrail)) {
				return thisTrail;
			}
		}
		return new Point(0, 0);
	}

	private boolean isTakenUpByLocationModel(Point point) {
		for (LocationModel locaitonModel: mTopModel.getAllLocationModels()) {
			if (locaitonModel.getCanvasLocation().equals(point)) {
				return true;
			}
		}
		return false;
	}


	private void registerShortcutKey() {
		//undo
		Action undo = new AbstractAction() {
			private static final long serialVersionUID = 1L;
			@Override
			public void actionPerformed(ActionEvent e) {
				undo();
			}
		};
		mCanvasPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control Z"), "undo");
		mCanvasPane.getActionMap().put("undo", undo);
		mChartsTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control Z"), "undo");
		mChartsTable.getActionMap().put("undo", undo);

		//redo
		Action redo = new AbstractAction() {
			private static final long serialVersionUID = 1L;
			@Override
			public void actionPerformed(ActionEvent e) {
				redo();
			}
		};
		mCanvasPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control Y"), "redo");
		mCanvasPane.getActionMap().put("redo", redo);
		mChartsTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control Y"), "redo");
		mChartsTable.getActionMap().put("redo", redo);

		//insert new chart
		Action insertNewChart = new AbstractAction() {
			private static final long serialVersionUID = 1L;
			@Override
			public void actionPerformed(ActionEvent e) {
				insertNewLocation();
			}
		};
		mCanvasPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control N"), "insert new chart");
		mCanvasPane.getActionMap().put("insert new chart", insertNewChart);
		mChartsTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control N"), "insert new chart");
		mChartsTable.getActionMap().put("insert new chart", insertNewChart);


	}

	private void showView() {
		mGraphFrame.pack();
		mGraphFrame.setVisible(true);	

		mTableFrame.pack();
		mTableFrame.setVisible(true);
	}

	//non undo-redo-able methods
	public void redo() {
		mCommandBroker.redo();
	}

	public void undo() {
		mCommandBroker.undo();
	}

	public TopModel getTopModel() {
		return mTopModel;
	}

	public void newDocument() {
		if (mSaveState.isDirty()) {
			if (promptSave() == CANCEL) {
				return;
			}
		}

		//open a new blank one
		new ChartCraftController();

		//close the current one 
		release();

	}

	public void quit() {
		if (mSaveState.isDirty()) {
			if (promptSave() == CANCEL) {
				return;
			}
		}

		System.exit(0);
	}

	public void open() {
		if (mSaveState.isDirty()) {
			if (promptSave() == CANCEL) {
				return;
			}
		}

		//get new file
		int returnVal = fileChooser.showOpenDialog(mTableFrame);
		if (returnVal != JFileChooser.APPROVE_OPTION) {
			return;
		} 

		File file = fileChooser.getSelectedFile();
		if (ChartCraftController.OpenFromFile(file) == null) {
			return;
		}

		//close the current one
		release();		
	}

	private void release() {
		//TODO:
		mGraphFrame.setVisible(false);
		mTableFrame.setVisible(false);

		mGraphFrame.removeAll();
		mTableFrame.removeAll();
	}



	public boolean promptSave() {
		String question;
		String[] options = {"Cancel", "Don't Save", "Save"};

		File destFile = mSaveState.getCurrentDocument();
		if (destFile == null) {
			question = "Do you want to save the changes you made?";
			options[2] = "Save...";
		} else {
			question = "Do you want to save the changes you made in the document " + destFile.getName() + "?";
		}


		int retVal = JOptionPane.showOptionDialog(mTableFrame, question, APP_NAME,
				JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
				options, options[2]);

		if (retVal == 0 || retVal == JOptionPane.CLOSED_OPTION) {
			return CANCEL;
		} else if (retVal == 1) {
			//no save
			return DONE;
		} else {
			//save 
			if (destFile == null) {
				int returnVal = fileChooser.showSaveDialog(mGraphFrame);
				if (returnVal != JFileChooser.APPROVE_OPTION) {
					return CANCEL;
				} else {
					destFile = fileChooser.getSelectedFile();
				}
			}

			//now know where to save
			return serializeToFile(destFile);

		}
	}


	public void save() {
		File currentDocument = mSaveState.getCurrentDocument();
		if (currentDocument != null) {
			serializeToFile(currentDocument);
		} else {
			saveAs();
		}
	}

	public void saveAs() {
		int returnVal = fileChooser.showSaveDialog(mGraphFrame);
		if (returnVal == JFileChooser.APPROVE_OPTION) {
			File file = fileChooser.getSelectedFile();
			serializeToFile(file);
		}
	}

	public boolean serializeToFile(File file) {
		FileOutputStream fos = null;
		ObjectOutputStream out = null;
		try {
			fos = new FileOutputStream(file);
			out = new ObjectOutputStream(fos);
			out.writeObject(mTopModel);
			out.close();
		} catch(IOException ex) {
			JOptionPane.showMessageDialog(null,
					"Failed to save the document.",
					"Error",
					JOptionPane.ERROR_MESSAGE);
			return CANCEL;
		}

		mSaveState.resetCurrentDocument(file);
		return DONE;
	}

	public void changeBackgroundImage() {
		imageFileChooser.setAcceptAllFileFilterUsed(false);
		imageFileChooser.setFileFilter(new ImageFilter());

		int returnVal = imageFileChooser.showOpenDialog(mGraphFrame);
		if (returnVal == JFileChooser.APPROVE_OPTION) {
			File file = imageFileChooser.getSelectedFile();
			insertBackgroundImage(file);
		}
	}

	public void deselectAllLocations() {
		final ArrayList<LocationModel> selectedLocationModels = mTopModel.getSelectedLocationModels();
		for (LocationModel selectedModel: selectedLocationModels) {
			selectedModel.setSelect(false);
		}
		selectedLocationModels.clear();
		mTopModel.setBackgroundSelected(false);

	}

	public void setImageHidding(boolean hiding) {
		mTopModel.setImageHiding(hiding);
	}

	/**
	 * 
	 * @param startIndex inclusively
	 * @param endIndex exclusively
	 */
	public void setSelectionOnly(int startIndex, int endIndex) {
		deselectAllLocations();

		//select the location model in range
		final ArrayList<LocationModel> selectedLocationModels = mTopModel.getSelectedLocationModels();
		final ArrayList<LocationModel> allCurrentLocationModels = mTopModel.getAllLocationModels();
		for (int index = startIndex; index < endIndex; index++) {
			final LocationModel newSelectedModel = allCurrentLocationModels.get(index);
			newSelectedModel.setSelect(true);
			selectedLocationModels.add(newSelectedModel);
		}

	}


	public void selectBackgroundOnly() {
		deselectAllLocations();

		//select the background
		mTopModel.setBackgroundSelected(true);
	}

	public void changeSelectionTo(int startIndex, int endIndex, boolean value) {
		final ArrayList<LocationModel> selectedLocationModels = mTopModel.getSelectedLocationModels();
		final ArrayList<LocationModel> allCurrentLocationModels = mTopModel.getAllLocationModels();
		for (int index = startIndex; index < endIndex; index++) {
			final LocationModel model = allCurrentLocationModels.get(index);
			if (model.isSelect() == value) {
				//do nothing
			} else {
				if (value) {
					model.setSelect(true);
					selectedLocationModels.add(model);
				} else {
					model.setSelect(false);
					selectedLocationModels.remove(model);
				}
			}
		}		
	}

	public void changeTimeTo(int time) {
		mTopModel.setCurrentTimeId(time);
		final List<LocationModel> allLocationModels =  mTopModel.getAllLocationModels();
		for (LocationModel locationModel: allLocationModels) {
			locationModel.setTime(time);
		}
	}

	public boolean isPlaying() {
		return mPlayer != null;
	}

	public void startPlaying() {
		mTopModel.setPlaying(true);
		mTopModel.setFrameNo(0.0);		

		final List<LocationModel> allLocationModels =  mTopModel.getAllLocationModels();
		for (LocationModel locationModel: allLocationModels) {
			locationModel.setPlaying(true);
			locationModel.setFrameNo(0.0);
		}


		mPlayer = new Timer();
		mPlayer.schedule(new PlayTask(), 0, PlayTask.PLAY_INTERVAL);
	}

	public void stopPlaying() {
		if (mPlayer != null) {
			mPlayer.cancel();
			mPlayer = null;
		}
		mTopModel.setPlaying(false);
		final List<LocationModel> allLocationModels =  mTopModel.getAllLocationModels();
		for (LocationModel locationModel: allLocationModels) {
			locationModel.setPlaying(false);
		}
	}

	private class PlayTask extends TimerTask {
		public static final long PLAY_INTERVAL = 30;
		public static final int MAIN_FRAME_PER_SECOND = 2;
		@Override
		public void run() {
			final double currentFrame = mTopModel.getFrameNo();			
			final double nextFrame = currentFrame + PLAY_INTERVAL * MAIN_FRAME_PER_SECOND/1000.0;

			if (nextFrame <= mTopModel.getNoOfTime() - 1) {

				mTopModel.setFrameNo(nextFrame);
				final List<LocationModel> allLocationModels =  mTopModel.getAllLocationModels();
				for (LocationModel locationModel: allLocationModels) {
					locationModel.setFrameNo(nextFrame);
				}

			} else {
				stopPlaying();
			}

			Log.v(this, "frameNo: " + nextFrame);
		}
	}

	//Undo-Redo-able methods
	public void deleteLocation(LocationModel locationModel) {
		mCommandBroker.executeNewCommand(new DeleteLocation(locationModel));		
	}

	public void bringToFront(LocationModel mModel) {
		int currentDepth = mTopModel.getDepthOf(mModel);
		int newDepth = 0;
		mCommandBroker.executeNewCommand(new ChangeDepth(currentDepth, newDepth));

	}

	public void bringToBack(LocationModel mModel) {
		int currentDepth = mTopModel.getDepthOf(mModel);
		int newDepth = mTopModel.getNoOfDepth() -1;
		mCommandBroker.executeNewCommand(new ChangeDepth(currentDepth, newDepth));		
	}

	public void changeSeriesMetaInformation(int seriesId, Color color, String columnName) {
		if (columnName == null || columnName.equals("")) {
			columnName = " ";
		}
		mCommandBroker.executeNewCommand(new ChangeSeriesMetaInforamtion(seriesId, color, columnName));
	}

	public void changeChartTypeTo(int chartType) {
		mCommandBroker.executeNewCommand(new ChangeChartType(chartType));
	}
	public void insertBackgroundImage(File imageFile) {
		if (!imageFile.canRead()) {
			JOptionPane.showMessageDialog(null,
					"Failed to open the background image.",
					"Error",
					JOptionPane.ERROR_MESSAGE);
		} 
		//read the file into imageData
		Image imageData = null;

		try {
			imageData = ImageIO.read(imageFile);

		} catch (Exception e) {
			imageData = null;
			JOptionPane.showMessageDialog(null,
					"Failed to open the background image. (Only accepts image file types.)",
					"Error",
					JOptionPane.ERROR_MESSAGE);
		} 

		if (imageData != null) {
			mCommandBroker.executeNewCommand(new InsertNewBackground(imageData));
		}
	}

	public void deleteBackgroundImage() {
		mCommandBroker.executeNewCommand(new DeleteBackground());
	}

	public void addTime() {
		mCommandBroker.executeNewCommand(new AddTimeCommand());
	}

	public void removeTime() {
		mCommandBroker.executeNewCommand(new RemoveTimeCommand());
	}
	public void insertNewSeries() {
		mCommandBroker.executeNewCommand(new InsertNewSeriesCommand());
	}

	public void  deleteLastSeries() {
		if (mTopModel.getNoOfSeries() <= 1) {
			Log.e(this, "delete series called when only one or less series");
			return;
		}
		mCommandBroker.executeNewCommand(new RemoveLastSeriesCommand());
	}

	public void insertNewLocation() {
		LocationModel newModel = new LocationModel(getNextLocationPoint(), new Dimension(100, 100), mTopModel.getChartType());
		newModel.setName(getNextLocationName());

		// match the dimension
		int currentNoOfTime = mTopModel.getNoOfTime();
		int currentNoOfSeries = mTopModel.getNoOfSeries();
		for (int time = 1; time < currentNoOfTime; time++) {
			newModel.addNewTime(null);
		}
		for (int series = 0; series < currentNoOfSeries; series++) {
			newModel.insertNewSeries();
		}

		//pair it with a graph item
		mCommandBroker.executeNewCommand(new InsertNewLocationCommand(newModel));
	}

	public void deleteLastLocation() {
		if (mTopModel.getNoOfLocations() <= 1) {
			Log.e(this, "Delete is called when only one of less locaiton");
			return;
		}
		int lastLocationId = mTopModel.getNoOfLocations() -1;
		LocationModel locationModel = mTopModel.getAllLocationModels().get(lastLocationId);
		deleteLocation(locationModel);
	}

	public void moveAndResizeLocationModel(LocationModel locationModel, Point startPoint, Point endPoint, Dimension startSize, Dimension endSize) {
		Command moveCommand = new ResizeAndMoveLocationCommand(locationModel, startSize, endSize, startPoint, endPoint);
		mCommandBroker.executeNewCommand(moveCommand);
	}


	public void setLocationName(int locationOffset, String newName) {
		if (locationOffset < mTopModel.getNoOfLocations()) {
			LocationModel locationModel = mTopModel.getChildLocationModel(locationOffset);
			final String oldName = locationModel.getName();
			Command renameCommand = new RenameLocationCommand(locationModel, oldName, newName);
			mCommandBroker.executeNewCommand(renameCommand);
			return;
		}
		Log.e(this, "Invalud location offset in \"setLocationName\"");
	}

	public void setValueOfCurrentTimeAt(int locationOffset, int seriesOffset, int timeId, Object value) {
		if (locationOffset < mTopModel.getNoOfLocations()) {
			LocationModel locationModel = mTopModel.getChildLocationModel(locationOffset);
			final Double oldValue = locationModel.getValueAt(timeId, seriesOffset);


			String valueStr = (String) value;
			Double newValue = 0.0;
			if (valueStr == null || valueStr.equals("")) {
				newValue = 0.0;
			} else {
				try {
					newValue = Double.valueOf((String) value);
				} catch (Exception e) {
					JOptionPane.showMessageDialog(null,
							"Invalid data entry. (This field only accepts numbers.)",
							"Error",
							JOptionPane.ERROR_MESSAGE);
					return;
				}
			}

			Command changeValueCommand = new ChangeValueCommand(locationModel, seriesOffset, timeId, oldValue, newValue);
			mCommandBroker.executeNewCommand(changeValueCommand);
			return;
		}		
	}

	public void moveAndResizeBackground(Point startPoint, Point endPoint,
			Dimension startSize, Dimension endSize) {		
		mCommandBroker.executeNewCommand(new BackgroundResizeAndMoveCommand(startSize, endSize, startPoint, endPoint));		
	}

	private void updateBoundValue() {


		double maxValue = 0;
		double minValue = 0;
		double maxSumOfValueOnSameLocation = 0;

		//find the min and max value
		for (int time = 0; time < mTopModel.getNoOfTime(); time++) {
			for (int series = 0; series < mTopModel.getNoOfSeries(); series ++) {
				ArrayList<LocationModel> allLocations = mTopModel.getAllLocationModels();
				for (LocationModel locationModel: allLocations) {
					double value = locationModel.getValueAt(time, series);
					maxValue = Math.max(maxValue, value);
					minValue = Math.min(minValue, value);
				}
			}

			for (LocationModel locationModel: mTopModel.getAllLocationModels()) {
				double sumOfValueInThisLocation = 0;
				for (int series = 0; series < mTopModel.getNoOfSeries(); series ++) {
					sumOfValueInThisLocation += locationModel.getValueAt(time, series);
				}
				maxSumOfValueOnSameLocation = Math.max(maxSumOfValueOnSameLocation, sumOfValueInThisLocation);
			}
		}

		//set the min and max value
		for (LocationModel locationModel: mTopModel.getAllLocationModels()) {
			locationModel.setLowerBoundOfValue(minValue);
			locationModel.setUpperBoundOfValue(maxValue);
			locationModel.setMaxSumOfValueInSameLocation(maxSumOfValueOnSameLocation);
		}
	}

	//Undo-Redo Command
	private class DeleteLocation implements Command {
		private int mDepth;
		private LocationModel mLocationModel;
		private int mLocationOffset;

		public DeleteLocation(LocationModel locationModel) {
			mLocationModel = locationModel;
			mDepth = mTopModel.getDepthOf(locationModel);
			mLocationOffset = mTopModel.getLocationId(locationModel);
		}

		@Override
		public void doIt() {
			mTopModel.deleteLocationModel(mLocationModel);
			mTopModel.removeDepth(mLocationModel);
		}

		@Override
		public void undoIt() {
			mTopModel.insertNewLocationModel(mLocationModel, mLocationOffset);
			mTopModel.addDepth(mLocationModel, mDepth);
		}
	}

	private class ChangeDepth implements Command {
		private int mOldDepth;
		private int mNewDepth;

		public ChangeDepth(int oldDepth, int newDepth) {
			mOldDepth = oldDepth;
			mNewDepth = newDepth;
		}

		@Override
		public void doIt() {
			mTopModel.changeDepth(mOldDepth, mNewDepth);

		}

		@Override
		public void undoIt() {
			mTopModel.changeDepth(mNewDepth, mOldDepth);
		}
	}


	private class ChangeChartType implements Command {
		private int mNewChartType;
		private int mOldChartType;

		public ChangeChartType(int chartType) {
			mNewChartType = chartType;
			mOldChartType = mTopModel.getChartType();
		}

		@Override
		public void doIt() {
			mTopModel.setChartType(mNewChartType);
			for (LocationModel locationModel: mTopModel.getAllLocationModels()) {
				locationModel.setChartType(mNewChartType);
			}		
		}

		@Override
		public void undoIt() {
			mTopModel.setChartType(mOldChartType);
			for (LocationModel locationModel: mTopModel.getAllLocationModels()) {
				locationModel.setChartType(mOldChartType);
			}
		}
	}

	private class ChangeSeriesMetaInforamtion implements Command {
		private Color mOldColor;
		private Color mNewColor;
		private int mSeriesId;
		private String mNewSeriesName;
		private String mOldSeriesName;

		public ChangeSeriesMetaInforamtion(int seriesId, Color newColor, String newSeriesName) {
			mOldColor = mTopModel.getColorForSeries(seriesId);
			mSeriesId = seriesId;
			mNewColor = newColor;
			mNewSeriesName = newSeriesName;
			mOldSeriesName = mTopModel.getNameForSeries(mSeriesId);
		}

		@Override
		public void doIt() {
			mTopModel.setColorForSeries(mSeriesId, mNewColor);
			mTopModel.setNameForSeries(mSeriesId, mNewSeriesName);
		}

		@Override
		public void undoIt() {
			mTopModel.setColorForSeries(mSeriesId, mOldColor);
			mTopModel.setNameForSeries(mSeriesId, mOldSeriesName);
		}
	}

	private class InsertNewBackground implements Command {
		private Image mOldImage;
		private Image mNewImage;
		private Dimension mOldSize;
		private Dimension mNewSize;
		private Point mOldLocation;
		private Point mNewLocation;
		private boolean mOldHidingStatus;

		InsertNewBackground(Image newImage) {
			mNewImage = newImage;
			mOldImage = mTopModel.getBackgroundImage();
			mOldSize = mTopModel.getBackgroundSize();
			mOldHidingStatus = mTopModel.isImagieHiding();
			mNewSize = new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT);
			mOldLocation = mTopModel.getBackgroundLocation();
			mNewLocation = new Point(0,0);
		}

		@Override
		public void doIt() {
			mTopModel.setBackgroundImage(mNewImage);
			mTopModel.setBackgroundLocation(mNewLocation);
			mTopModel.setBackgroundSize(mNewSize);
			mTopModel.setImageHiding(false);
		}

		@Override
		public void undoIt() {
			mTopModel.setBackgroundImage(mOldImage);
			mTopModel.setBackgroundLocation(mOldLocation);
			mTopModel.setBackgroundSize(mOldSize);
			mTopModel.setImageHiding(mOldHidingStatus);
		}

	}

	private class DeleteBackground implements Command {
		private Image mOldImage;

		@Override
		public void doIt() {
			mOldImage = mTopModel.getBackgroundImage();
			mTopModel.setBackgroundImage(null);
		}

		@Override
		public void undoIt() {
			mTopModel.setBackgroundImage(mOldImage);
		}

	}

	private class InsertNewSeriesCommand implements Command {

		@Override
		public void doIt() {
			mTopModel.addColorForSeries(getNextColor());
			mTopModel.addNameForSeries(getNextSeriesName());
			final List<LocationModel> allLocationModels =  mTopModel.getAllLocationModels();
			for (LocationModel locationModel: allLocationModels) {
				locationModel.insertNewSeries();
			}
		}

		@Override
		public void undoIt() {
			final List<LocationModel> allLocationModels =  mTopModel.getAllLocationModels();
			for (LocationModel locationModel: allLocationModels) {
				locationModel.deleteLastSeries();
			}
			mTopModel.removeColorOfLastSeries();
			mTopModel.removeNameOfLastSeries();
		}
	}

	private class RemoveLastSeriesCommand implements Command {
		private ArrayList<ArrayList<Double>> valuesOfDifferentTimeAtDifferentLocaiton;
		private Color seriesColor;
		private String seriesName;


		@Override
		public void doIt() {
			valuesOfDifferentTimeAtDifferentLocaiton = new ArrayList<ArrayList<Double>>();

			final List<LocationModel> allLocationModels =  mTopModel.getAllLocationModels();
			for (LocationModel locationModel: allLocationModels) {
				valuesOfDifferentTimeAtDifferentLocaiton.add(locationModel.deleteLastSeries());
			}

			seriesColor = mTopModel.removeColorOfLastSeries();
			seriesName = mTopModel.removeNameOfLastSeries();
		}

		@Override
		public void undoIt() {
			mTopModel.addColorForSeries(seriesColor);//get next color
			mTopModel.addNameForSeries(seriesName); //get next name

			final List<LocationModel> allLocationModels =  mTopModel.getAllLocationModels();
			for (int locationId = 0; locationId < allLocationModels.size(); locationId++) {
				LocationModel locationModel = allLocationModels.get(locationId);
				ArrayList<Double> values = valuesOfDifferentTimeAtDifferentLocaiton.get(locationId);
				locationModel.insertSeriesWithValue(values);
			}		
		}

	}

	private class AddTimeCommand implements Command {
		private int mOldDisplayingTime;

		@Override
		public void doIt() {
			final int newNoOfTime = mTopModel.getNoOfTime() + 1;
			final int latestTimeId = newNoOfTime -1;
			mOldDisplayingTime = mTopModel.getCurrentTimeId();

			final List<LocationModel> allLocationModels = mTopModel.getAllLocationModels();
			for (LocationModel locationModel: allLocationModels) {
				locationModel.addNewTime(null);
			}

			mTopModel.setNoOfTime(newNoOfTime);
			changeTimeTo(latestTimeId);
		}

		@Override
		public void undoIt() {
			final int oldNoOfTime = mTopModel.getNoOfTime() -1;
			changeTimeTo(mOldDisplayingTime);
			mTopModel.setNoOfTime(oldNoOfTime);

			final List<LocationModel> allLocationModels = mTopModel.getAllLocationModels();
			for (LocationModel locationModel: allLocationModels) {
				locationModel.removeLastTime();
			}

		}
	}

	private class RemoveTimeCommand implements Command {
		int mTimeBeforeRemove;
		ArrayList<ArrayList<Double>> mLocationsDatasetRemoved = new ArrayList<ArrayList<Double>>(); 

		@Override
		public void doIt() {			
			//set the displaying time
			mTimeBeforeRemove = mTopModel.getCurrentTimeId();
			final int latestTimeId = mTopModel.getNoOfTime() -1;
			if (mTopModel.getCurrentTimeId() == latestTimeId) {
				changeTimeTo(latestTimeId -1);
			}

			//update the no. of time
			mTopModel.setNoOfTime(mTopModel.getNoOfTime() - 1);

			//store and remove the latest dataset
			mLocationsDatasetRemoved.clear();
			final List<LocationModel> allLocationModels = mTopModel.getAllLocationModels();
			for (LocationModel locationModel: allLocationModels) {
				final ArrayList<Double> datasetRemoved = locationModel.removeLastTime();
				mLocationsDatasetRemoved.add(datasetRemoved);
			}

			updateBoundValue();
		}

		@Override
		public void undoIt() {

			final List<LocationModel> allLocationModels = mTopModel.getAllLocationModels();
			for (int i = 0; i < allLocationModels.size(); i++) {
				final LocationModel locationModel = allLocationModels.get(i);
				final ArrayList<Double> removedDataset = mLocationsDatasetRemoved.get(i);
				locationModel.addNewTime(removedDataset);
			}

			mTopModel.setNoOfTime(mTopModel.getNoOfTime() + 1);

			changeTimeTo(mTimeBeforeRemove);
			updateBoundValue();
		}

	}

	private class InsertNewLocationCommand implements Command {
		private final LocationModel mNewLocation;

		InsertNewLocationCommand(LocationModel newLocation) {
			mNewLocation = newLocation;
		}

		@Override
		public void doIt() {
			mTopModel.insertNewLocationModel(mNewLocation);
			mTopModel.addDepth(mNewLocation, 0);
			updateBoundValue();
		}

		@Override
		public void undoIt() {
			mTopModel.deleteLocationModel(mNewLocation);
			mTopModel.removeDepth(mNewLocation);
			updateBoundValue();
		}

	}

	private class ResizeAndMoveLocationCommand implements Command {
		private final LocationModel mLocationModel;
		private final Dimension mStartSize;
		private final Dimension mEndSize;
		private final Point mStartLocation;
		private final Point mEndLocation;

		ResizeAndMoveLocationCommand(LocationModel locationModel, 
				Dimension startSize, Dimension endSize,
				Point startLocation, Point endLocation) {
			mLocationModel = locationModel;
			mStartSize = startSize;
			mEndSize = endSize;
			mStartLocation = startLocation;
			mEndLocation = endLocation;
		}

		@Override
		public void doIt() {
			mLocationModel.setSize(mEndSize);
			mLocationModel.setLocation(mEndLocation);
		}

		@Override
		public void undoIt() {
			mLocationModel.setSize(mStartSize);
			mLocationModel.setLocation(mStartLocation);
		}
	}

	private class RenameLocationCommand implements Command {
		private final LocationModel mNewLocation;
		private final String mOldName;
		private final String mNewName;

		RenameLocationCommand(LocationModel newLocation, String oldName, String newName) {
			mNewLocation = newLocation;
			mOldName = oldName;
			mNewName = newName;
		}

		@Override
		public void doIt() {
			mNewLocation.setName(mNewName);
		}

		@Override
		public void undoIt() {
			mNewLocation.setName(mOldName);
		}

	}

	private class ChangeValueCommand implements Command {
		private final LocationModel mNewLocation;
		private final Double mNewValue;
		private final Double mOldValue;
		private final int mTimeId;
		private final int mSeriesOffset;

		ChangeValueCommand(LocationModel newLocation, int seriesOffset, int timeId, Double oldValue, Double newValue) {
			mNewLocation = newLocation;
			mOldValue = oldValue;
			mNewValue = newValue;
			mSeriesOffset = seriesOffset;
			mTimeId = timeId;
		}

		@Override
		public void doIt() {
			mNewLocation.setValueAt(mTimeId, mSeriesOffset, mNewValue);
			changeTimeTo(mTimeId);
			updateBoundValue();
		}

		@Override
		public void undoIt() {
			mNewLocation.setValueAt(mTimeId, mSeriesOffset, mOldValue);
			changeTimeTo(mTimeId);
			updateBoundValue();
		}
	}

	private class BackgroundResizeAndMoveCommand implements Command {
		private final Dimension mStartSize;
		private final Dimension mEndSize;
		private final Point mStartLocation;
		private final Point mEndLocation;

		BackgroundResizeAndMoveCommand( Dimension startSize, Dimension endSize,
				Point startLocation, Point endLocation) {
			mStartSize = startSize;
			mEndSize = endSize;
			mStartLocation = startLocation;
			mEndLocation = endLocation;
		}

		@Override
		public void doIt() {
			selectBackgroundOnly();
			mTopModel.setBackgroundSize(mEndSize);
			mTopModel.setBackgroundLocation(mEndLocation);
		}

		@Override
		public void undoIt() {
			selectBackgroundOnly();
			mTopModel.setBackgroundSize(mStartSize);
			mTopModel.setBackgroundLocation(mStartLocation);
		}
	}
}

